﻿using System.Linq.Expressions;
using System.Reflection;
using K9Nano.Domain.Organization;
using K9Nano.Extensions;
using K9Nano.Session;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace K9Nano.Domain.Entities;

public abstract class K9NanoDbContextBase<TUser, TRole, TUserRole, TRolePermission> : DbContext
    where TUser : UserBase
    where TRole : RoleBase
    where TUserRole : UserRoleBase<TUser, TRole>
    where TRolePermission : RolePermissionBase<TRole>
{
    protected K9NanoDbContextBase(DbContextOptions options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        foreach (var entityType in GetConventionEntities())
        {
            modelBuilder.Entity(entityType, entity =>
            {
                entity.HasKey(nameof(IEntity.Id));
                entity.Property(nameof(IEntity.Id)).ValueGeneratedOnAdd();

                // Global Filters
                var filter = CreateQueryFilterByType(entityType);
                entity.HasQueryFilter(filter);

                // Naming Convention
                var currentTableName = entity.Metadata.GetTableName();
                entity.ToTable(GetConventionalTableName(currentTableName!),
                    t => t.HasComment(entityType.GetDescription()));
            });
        }

        modelBuilder.Entity<TUser>(et => et.HasIndex(a => a.Username).IsUnique());

        modelBuilder.Entity<TRole>(et => et.HasIndex(a => a.Code).IsUnique());

        modelBuilder.Entity<TUserRole>(et =>
        {
            et.HasIndex(a => new { a.UserId, a.RoleId }).IsUnique();
            et.HasQueryFilter(a => !a.User.IsDeleted);
        });

        modelBuilder.Entity<OrganizationUnit>(et =>
        {
            et.HasIndex(a => a.ParentId).HasDatabaseName("idx_parent");
            et.HasIndex(a => a.Code).IsUnique().HasDatabaseName("idx_unique_code");
        });
    }

    #region Convention

    /// <summary>
    /// Get conventional table name
    /// </summary>
    /// <param name="tableName"></param>
    /// <returns></returns>
    public virtual string GetConventionalTableName(string tableName)
    {
        return tableName.ToSingular().ToSnakeCase();
    }

    protected virtual IEnumerable<Assembly> GetConventionalAssemblies()
    {
        return AppDomain.CurrentDomain.GetAssemblies()
                .Where(a => !a.IsDynamic
                && !a.IsCollectible
                && a.FullName?.StartsWith("System") != true
                && a.FullName?.StartsWith("Microsoft") != true);
    }

    protected virtual IReadOnlyList<Type> GetConventionEntities()
    {
        return GetConventionalAssemblies()
                .SelectMany(a => a.DefinedTypes)
                .Where(a => a is { IsClass: true, IsPublic: true, IsAbstract: false }
                           && a.IsAssignableTo(typeof(IEntity)))
                .ToList();
    }

    protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
    {
        // configurationBuilder.Conventions.Add(_ => new MaxStringLengthConvention());
    }

    #endregion

    #region Global Filters

    private static MethodInfo? _createQueryFilterMethodInfo;

    protected MethodInfo CreateQueryFilterMethodInfo
        => _createQueryFilterMethodInfo ??= GetType().GetMethod(nameof(CreateQueryFilter), BindingFlags.Instance | BindingFlags.NonPublic)!;

    protected virtual LambdaExpression CreateQueryFilterByType(Type entityClrType)
    {
        return (LambdaExpression)CreateQueryFilterMethodInfo
              .MakeGenericMethod(entityClrType)
              .Invoke(this, [])!;
    }

    protected virtual LambdaExpression? CreateQueryFilter<TEntity>()
        where TEntity : class
    {
        Expression<Func<TEntity, bool>>? filterExpression = null;
        var soft = typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity));
        if (soft)
        {
            filterExpression = p => !((ISoftDelete)p).IsDeleted; filterExpression = p => !((ISoftDelete)p).IsDeleted;
        }
        return filterExpression;
    }

    #endregion

    #region SaveChanges

    public override int SaveChanges(bool acceptAllChangesOnSuccess)
    {
        BeforeSaveChanges();
        var result = base.SaveChanges(acceptAllChangesOnSuccess);
        AfterSaveChanges();
        return result;
    }

    public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken())
    {
        BeforeSaveChanges();
        var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
        AfterSaveChanges();
        return result;
    }

    protected virtual void BeforeSaveChanges()
    {
        var currentUserId = this.GetService<IUserContextAccessor>().CurrentUser?.UserId;

        foreach (var entry in ChangeTracker.Entries())
        {
            switch (entry.State)
            {
                case EntityState.Detached:
                case EntityState.Unchanged:
                    continue;
                case EntityState.Deleted:
                    if (entry.Entity is ISoftDelete)
                    {
                        entry.State = EntityState.Modified;
                        var isDeleted = entry.Property(nameof(ISoftDelete.IsDeleted));
                        isDeleted.OriginalValue = isDeleted.CurrentValue = true;

                        var deletedAt = entry.Property(nameof(ISoftDelete.DeletedAt));
                        deletedAt.OriginalValue = deletedAt.CurrentValue = DateTime.Now;

                        var deletedBy = entry.Property(nameof(ISoftDelete.DeletedBy));
                        deletedBy.OriginalValue = deletedBy.CurrentValue = currentUserId;
                    }
                    break;
                case EntityState.Modified:
                    if (entry.Entity is IAuditEntity)
                    {
                        var updatedBy = entry.Property(nameof(IAuditEntity.UpdatedBy));
                        updatedBy.OriginalValue = updatedBy.CurrentValue = currentUserId;

                        var updatedAt = entry.Property(nameof(IAuditEntity.UpdatedAt));
                        updatedAt.OriginalValue = updatedAt.CurrentValue = DateTime.Now;
                    }
                    break;
                case EntityState.Added:
                    if (entry.Entity is IEntity)
                    {
                        var createdAt = entry.Property(nameof(IEntity.CreatedAt));
                        createdAt.OriginalValue = createdAt.CurrentValue = DateTime.Now;

                        if (entry.Entity is IAuditEntity)
                        {
                            var createdBy = entry.Property(nameof(IAuditEntity.CreatedBy));
                            createdBy.OriginalValue = createdBy.CurrentValue = currentUserId ?? 0L;
                        }
                    }

                    break;
            }
        }

    }

    protected virtual void AfterSaveChanges()
    {
    }

    #endregion
}
